package com.web.music.impl;

import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.web.music.common.MusicException;
import com.web.music.interfaces.XmlParser;
import com.web.music.model.Album;
import com.web.music.model.Artist;
import com.web.music.model.Catalog;
import com.web.music.model.Genre;
import com.web.music.model.MusicBand;
import com.web.music.model.Person;
import com.web.music.model.Singer;
import com.web.music.xml.MusicXmlErrorHandler;
import com.web.music.xml.XmlSchemaConst;

public class DomXmlParser implements XmlParser {
	
	static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
	static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema"; 
	static final String JAXP_SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource";	
	
	private DocumentBuilder getDocumentBuilder(){
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();			
			// XML document will be validated against XML Schema (XSD file)
			// if document will not correspond to schema definition, then there will be an error
			factory.setNamespaceAware(true);
			factory.setValidating(true);
			try {
			  factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
			} 
			catch (IllegalArgumentException e) {
			  throw new MusicException(e);
			} 
			DocumentBuilder db = factory.newDocumentBuilder();
			// define error handle class to report XML processing errors
			db.setErrorHandler(new MusicXmlErrorHandler());
			return db;
		} catch (ParserConfigurationException e) {
			throw new MusicException(e);
		}
		
	}
	
	public Map<String,Person> extractPersons(InputStream input){
		Document doc = null;
		try {
			doc = getDocumentBuilder().parse(input);
		} catch (Exception e) {
			throw new MusicException("Problem parsing the file: " + e);
		}
		return extractPersons(doc);
	}
	
	public Map<String,Person> extractPersons(File xmlFile){
		Document doc = null;
		try {			
			doc = getDocumentBuilder().parse(xmlFile);
		} catch (Exception e) {
			throw new MusicException("Problem parsing the file: " + e);
		}
		return extractPersons(doc);
	}
	
	private Map<String,Person> extractPersons(Document doc)
	{
		Map<String,Person> persons = new HashMap<String,Person>();

		// go through the Document and extract persons info
		Element root = doc.getDocumentElement();
		for (Node personNode = root.getFirstChild(); personNode != null; personNode = personNode.getNextSibling())
		{	
			if (!XmlSchemaConst.Entities.PERSON.equals(personNode.getNodeName())){
				System.out.println("Node is not 'person' node: " + personNode);
				continue;
			}
			NamedNodeMap attrMap = personNode.getAttributes();
			if (attrMap == null){
				System.out.println("Node doesn't have attributes: " + personNode);
				continue;
			}
			
			Person person = new Person();
			Node idNode = attrMap.getNamedItem(XmlSchemaConst.Person.ID);
			if (idNode == null){
				System.out.println("Person node attribute 'id' is null: " + personNode);
				continue;
			}
			
			String personIdString = idNode.getNodeValue();
			person.setId(extractIdValue(personIdString, XmlSchemaConst.IdPrefixes.PERSON_ID_PREFIX));
			
			// read remaining person attributes
			NodeList personNodes = personNode.getChildNodes();
			for (int i=0; i<personNodes.getLength(); i++){
				Node node = personNodes.item(i);
				if (XmlSchemaConst.Person.NAME.equals(node.getNodeName())){
					person.setName(node.getTextContent());	
					continue;
				}
				if (XmlSchemaConst.Person.SURNAME.equals(node.getNodeName())){
					person.setSurname(node.getTextContent());
					continue;
				}
			}
			persons.put(personIdString,person);
		}
		return persons;
	}	
	
	public Catalog extractCatalog(File xmlFile, Map<String, Person> persons) {
		Document doc = null;
		try {
			doc = getDocumentBuilder().parse(xmlFile);
		} catch (Exception e) {
			throw new MusicException("Problem parsing the file: " + e);
		}
		return extractCatalog(doc, persons);
	}
	
	public Catalog extractCatalog(InputStream input, Map<String, Person> persons) {
		Document doc = null;
		try {
			doc = getDocumentBuilder().parse(input);
		} catch (Exception e) {
			throw new MusicException("Problem parsing the file: " + e);
		}
		return extractCatalog(doc, persons);
	}
	
	private Catalog extractCatalog(Document doc, Map<String, Person> persons) 
	{
		Catalog catalog = new Catalog();
		Map<String,Genre> genres = new HashMap<String,Genre>();
		Map<String,Artist> artists = new HashMap<String,Artist>();
		
		Element root = doc.getDocumentElement();		
		for (Node node = root.getFirstChild(); node != null; node = node.getNextSibling())
		{	
			if (XmlSchemaConst.Entities.GENRES.equals(node.getNodeName())){
				System.out.println("Inside <genres>");
				genres = extractGenres(node);	
				catalog.setGenres(new ArrayList<Genre>(genres.values()));
				System.out.println("<genres> processed");
				continue;
			}
			else if (XmlSchemaConst.Entities.ARTISTS.equals(node.getNodeName())){
				System.out.println("Inside <artists>");
				artists = extractArtists(node, genres, persons);
				catalog.setArtists(new ArrayList<Artist>(artists.values()));
				System.out.println("<artists> processed");
			}
			else if (XmlSchemaConst.Entities.ALBUMS.equals(node.getNodeName())){
				System.out.println("Inside <albums>");
				List<Album> albums = extractAlbums(node, genres, artists);
				catalog.setAlbums(albums);
				System.out.println("<albums> processed");
			}
			else{
				System.out.println("Inside unkown tag < " + node.getNodeName() + " > ");
			}
		}			
		System.out.println("[processCatalog] Finished");	
		return catalog;	
	}

	/**
	 * Extracts information about artists and creates a map with corresponding objects
	 * 
	 * @param node - <persons> node
	 * @param genres - a map with genres for references
	 * @param persons - a map with persons for references
	 * @return a map <artist_id, artist>
	 */
	private Map<String, Artist> extractArtists(Node node, Map<String,Genre> genres, Map<String,Person> persons) 
	{
		Map<String,Artist> artists = new HashMap<String,Artist>();
		
		for (Node artistNode = node.getFirstChild(); artistNode != null; artistNode = artistNode.getNextSibling()) 
		{	
			if (!XmlSchemaConst.Entities.ARTIST.equals(artistNode.getNodeName())){
				System.out.println("Node is not <artist> node: " + artistNode);
				continue;
			}
			
			String artistIdString = getNodeAttributeValue(artistNode, XmlSchemaConst.Artist.ID);
			if (artistIdString == null){
				System.out.println("Album node attribute 'id' is not resolved: " + artistNode);
				continue;
			}
			String artistType = getNodeAttributeValue(artistNode, XmlSchemaConst.Artist.TYPE);
			if (artistType == null){
				System.out.println("Album node attribute 'type' is not resolved: " + artistNode);
				continue;
			}
			
			Artist artist = null;
			if (XmlSchemaConst.Artist.TYPE_SINGER.equals(artistType)){
				artist = new Singer();
			}
			else if(XmlSchemaConst.Artist.TYPE_BAND.equals(artistType)){				
				artist = new MusicBand();
			}
			artist.setId(extractIdValue(artistIdString, XmlSchemaConst.IdPrefixes.ARTIST_ID_PREFIX));
						
			NodeList artistNodes = artistNode.getChildNodes();
			for (int i=0; i<artistNodes.getLength(); i++){
				Node attributeNode = artistNodes.item(i);				
				
				if (artist instanceof Singer){
					if (XmlSchemaConst.Artist.NAME.equals(attributeNode.getNodeName())){
						((Singer)artist).setName(attributeNode.getTextContent());
						continue;
					}
					if (XmlSchemaConst.Artist.SINGER_ATTRIBUTE.equals(attributeNode.getNodeName())){
						((Singer)artist).setSingerAttribute(attributeNode.getTextContent());
						continue;
					}
					if (XmlSchemaConst.Artist.PERSON.equals(attributeNode.getNodeName())){
						String personId = attributeNode.getTextContent();
						if (personId != null){
							Person person = persons.get(personId);
							if (person != null){
								((Singer)artist).setPerson(person);
							}
						}			
					}
				}
				else if (artist instanceof MusicBand){
					if (XmlSchemaConst.Artist.TITLE.equals(attributeNode.getNodeName())){
						((MusicBand)artist).setTitle(attributeNode.getTextContent());
						continue;
					}
					if (XmlSchemaConst.Artist.BAND_ATTRIBUTE.equals(attributeNode.getNodeName())){
						((MusicBand)artist).setBandAttribute(attributeNode.getTextContent());
						continue;
					}
					if (XmlSchemaConst.Artist.MEMBER.equals(attributeNode.getNodeName())){
						String personId = attributeNode.getTextContent();
						if (personId != null){
							Person person = persons.get(personId);
							if (person != null){
								((MusicBand)artist).getMembers().add(person);
							}
						}			
					}
				}
				
				if (XmlSchemaConst.Artist.GENRE.equals(attributeNode.getNodeName())){
					String genreIdref = getNodeAttributeValue(attributeNode, XmlSchemaConst.IDREF);
					if (genreIdref != null){
						Genre genre = genres.get(genreIdref);
						if (genre != null){
							artist.getGenres().add(genre);
						}
					}			
				}

			}
			artists.put(artistIdString, artist);
		}	
		
		return artists;
	}

	/**
	 * Extracts information about albums and creates a map with corresponding objects
	 * 
	 * @param node - <albums> node
	 * @param genres - a map with genres for references
	 * @param artists - a map with artists for references
	 * @return a map <album_id, album>
	 */
	private List<Album> extractAlbums(Node node, Map<String,Genre> genres, Map<String,Artist> artists) 
	{
		List<Album> albums = new ArrayList<Album>();
		
		for (Node albumNode = node.getFirstChild(); albumNode != null; albumNode = albumNode.getNextSibling()) 
		{	
			if (!XmlSchemaConst.Entities.ALBUM.equals(albumNode.getNodeName())){
				System.out.println("Node is not <album> node: " + albumNode);
				continue;
			}
			
			String albumIdString = getNodeAttributeValue(albumNode, XmlSchemaConst.Album.ID);
			if (albumIdString == null){
				System.out.println("Album node attribute 'id' is not resolved: " + albumNode);
				continue;
			}
			
			Album album = new Album();
			album.setId(extractIdValue(albumIdString, XmlSchemaConst.IdPrefixes.ALBUM_ID_PREFIX));
			
			NodeList albumNodes = albumNode.getChildNodes();
			for (int i=0; i<albumNodes.getLength(); i++){
				Node attributeNode = albumNodes.item(i);
				if (XmlSchemaConst.Album.TITLE.equals(attributeNode.getNodeName())){
					album.setTitle(attributeNode.getTextContent());	
					continue;
				}	
				if (XmlSchemaConst.Album.GENRE.equals(attributeNode.getNodeName())){
					String genreIdref = getNodeAttributeValue(attributeNode, XmlSchemaConst.IDREF);
					if (genreIdref != null){
						Genre genre = genres.get(genreIdref);
						if (genre != null){
							album.getGenres().add(genre);
						}
					}			
				}
				if (XmlSchemaConst.Album.ARTIST.equals(attributeNode.getNodeName())){
					String artistIdref = getNodeAttributeValue(attributeNode, XmlSchemaConst.IDREF);
					if (artistIdref != null){
						Artist artist = artists.get(artistIdref);
						if (artist != null){
							album.setArtist(artist);
						}
					}			
				}
			}
			albums.add(album);
		}	
		
		return albums;
	}

	/**
	 * Extracts information about genres and creates a map with corresponding objects
	 * 
	 * @param node - <genres> node
	 * @return a map <genre_id, genre>
	 */
	private Map<String, Genre> extractGenres(Node node) 
	{
		Map<String,Genre> genres = new HashMap<String,Genre>();
		for (Node genreNode = node.getFirstChild(); genreNode != null; genreNode = genreNode.getNextSibling()) 
		{	
			if (!XmlSchemaConst.Entities.GENRE.equals(genreNode.getNodeName())){
				System.out.println("Node is not <genre> node: " + genreNode);
				continue;
			}
			
			String genreIdString = getNodeAttributeValue(genreNode, XmlSchemaConst.Genre.ID);
			if (genreIdString == null){
				System.out.println("Genre node attribute 'id' is not resolved: " + genreNode);
			}
			
			Genre genre = new Genre();
			genre.setId(extractIdValue(genreIdString, XmlSchemaConst.IdPrefixes.GENRE_ID_PREFIX));
			
			NodeList genreNodes = genreNode.getChildNodes();
			for (int i=0; i<genreNodes.getLength(); i++){
				Node attributeNode = genreNodes.item(i);
				if (XmlSchemaConst.Person.NAME.equals(attributeNode.getNodeName())){
					genre.setName(attributeNode.getTextContent());	
					continue;
				}	
			}
			genres.put(genreIdString, genre);
		}	
		return genres;
	}
	
	private String getNodeAttributeValue(Node node, String attrName)
	{
		NamedNodeMap attrMap = node.getAttributes();
		if (attrMap != null){
			Node attrNode = attrMap.getNamedItem(attrName);
			if (attrNode != null){
				return attrNode.getNodeValue();
			}
		}
		return null;
	}
	
	/**
	 * Id values in XML file are in a form: [id_prefix][long value]
	 * Extracts id long value from full string
	 * @param nodeValue - full id string
	 * @param prefix - id prefix
	 * @return - id Long value to be saved in DB
	 */
	private Long extractIdValue(String nodeValue, String prefix) {
		if (nodeValue.startsWith(prefix)){
			String idValue = nodeValue.substring(prefix.length(), nodeValue.length());
			try {
				return Long.parseLong(idValue);
			} catch (NumberFormatException e) {
				throw new MusicException("Wrong ID format: " + nodeValue);
			}
		}
		else{
			throw new MusicException("Wrong ID format: " + nodeValue);
		}
	}
}